# 用户输入处理与消息格式化

> **一句话摘要**：从用户键入到 API 调用的完整链路——经历输入路由、消息构造、附件注入、10+ 步标准化管道，支持 5 种消息类型和 30+ 种附件类型的精确格式化。

> 核心文件：`src/utils/messages.ts`、`src/utils/processUserInput/`、`src/query.ts`

## 一、用户输入到 API 的完整链路

```
用户键入
    │
    ▼
handlePromptSubmit()          ← 入口：处理退出命令、解析引用、过滤图片
    │
    ▼
processUserInput()            ← 核心路由
    ├── 以 / 开头 → processSlashCommand()（命令或 Skill）
    ├── mode=bash → processBashCommand()
    └── 其它 → processTextPrompt()（普通文本）
                  │
                  ▼
              createUserMessage()   ← 构造 UserMessage + 图片块 + 附件
                  │
                  ▼
              query()               ← 主查询循环（多轮工具调用）
                  │
                  ├── microcompact / autocompact（上下文管理）
                  ├── 附件注入（记忆、IDE 选区、hook 上下文）
                  ├── context collapse
                  │
                  ▼
              normalizeMessagesForAPI()  ← API 前标准化
                  │
                  ├── 过滤 system/progress/virtual 消息
                  ├── 合并连续 user 消息（Bedrock 兼容）
                  ├── strip tool_reference/失败图片/PDF 块
                  │
                  ▼
              queryModelWithStreaming()  ← 实际 API 调用
```

## 二、5 种消息类型

| 类型 | 创建函数 | 用途 |
|------|---------|------|
| **UserMessage** | `createUserMessage()` | 用户输入、工具结果 |
| **AssistantMessage** | `createAssistantMessage()` | 模型回复、API 错误 |
| **SystemMessage** | `createSystemMessage()` | 系统通知(info/warning/error) |
| **AttachmentMessage** | `createAttachmentMessage()` | 记忆、hook、IDE 上下文 |
| **ProgressMessage** | — | 流式进度 |

## 三、`<system-reminder>` 注入机制

系统上下文通过 `wrapInSystemReminder()` 注入到 user 消息中：

```xml
<system-reminder>
As you answer the user's questions, you can use the following context:

# claudeMd
Codebase and user instructions are shown below...
Contents of /path/CLAUDE.md (project instructions):
[CLAUDE.md 内容]

# currentDate
Today's date is 2026-03-31.

IMPORTANT: this context may or may not be relevant to your tasks.
</system-reminder>
```

**使用场景**：
- CLAUDE.md 内容注入
- 当前日期注入
- Skill 列表注入
- Agent 列表注入
- MCP 工具指令注入
- 文件读取时的安全提醒
- 记忆文件陈旧提醒

## 四、Slash Commands 系统

### 命令类型

```typescript
type PromptCommand = {
  type: 'prompt'
  progressMessage: string        // UI 进度提示
  contentLength: number          // 预估内容长度
  source: 'builtin' | 'mcp' | 'plugin' | 'bundled'
  context?: 'inline' | 'fork'   // inline=展开到对话 / fork=子 agent
  hooks?: HooksSettings          // 技能专属 hooks
  getPromptForCommand(args, context): Promise<ContentBlockParam[]>
}
```

### 命令解析流程

```
用户输入 "/compact some args"
    ↓
parseSlashCommand()  → { commandName: "compact", args: "some args" }
    ↓
findCommand()        → 匹配注册的 Command 对象
    ↓
command.load()       → 动态导入命令模块
    ↓
command.call()       → 执行命令逻辑
```

### 主要命令（70+）

**核心**：`/compact`, `/clear`, `/config`, `/cost`, `/help`, `/model`, `/plan`, `/exit`
**开发**：`/commit`, `/diff`, `/branch`, `/pr_comments`, `/review`
**Agent**：`/agents`, `/tasks`, `/skills`, `/fork`
**配置**：`/memory`, `/hooks`, `/permissions`, `/keybindings`, `/output-style`
**调试**：`/doctor`, `/stats`, `/context`, `/export`

## 五、Skill 系统

### Skill vs Command

Skill 本质上是 `type: 'prompt'` 的 Command，但通过 **SkillTool** 被模型自动调用（而非用户手动 `/`）。

### 4 种 Skill 来源

| 来源 | 路径 | 说明 |
|------|------|------|
| **Bundled** | 编译到 CLI | `update-config`, `verify`, `simplify`, `loop`, `schedule` 等 |
| **Skill Dir** | `.claude/skills/*.md` | 用户自定义 Markdown 技能 |
| **Plugin** | 插件系统 | 第三方扩展 |
| **MCP** | MCP 协议 | 外部服务暴露的 prompts |

### Bundled Skill 完整列表与触发条件

以下是 `initBundledSkills()` 中注册的所有 bundled skill：

| Skill 名称 | 描述 | whenToUse / 触发条件 | 限制 |
|------------|------|----------------------|------|
| **update-config** | 配置 settings.json（hooks、permissions、env） | 用户说"from now on when X"、"whenever X"、"allow X"、"set X=Y" 等自动化行为请求 | `allowedTools: ['Read']` |
| **keybindings-help** | 自定义键盘快捷键 | 用户要"rebind keys"、"add chord"、"customize keybindings" | 需 `isKeybindingCustomizationEnabled()` |
| **verify** | 验证代码更改是否正确 | 仅 ant 用户；通过 Markdown frontmatter 定义描述 | ant-only |
| **simplify** | 代码审查（reuse/quality/efficiency）→ 启动 3 个并行 Agent | 用户要 review 或 simplify 代码 | — |
| **debug** | 启用调试日志并诊断问题 | `disableModelInvocation: true`（仅用户手动 `/debug`） | `allowedTools: ['Read', 'Grep', 'Glob']` |
| **remember** | 审查 auto-memory，提议 promote 到 CLAUDE.md | "review/organize/promote auto-memory entries" | ant-only；需 `isAutoMemoryEnabled()` |
| **batch** | 并行工作编排（多分支 PR） | 用户要批量并行修改 | 动态计算 agent 数（5-30） |
| **stuck** | 诊断冻结/卡住的 CC 会话 | ant-only；调查进程状态 | ant-only |
| **skillify** | 将当前会话转化为可复用 skill | 用户想把当前流程保存为 skill | — |
| **loremIpsum** | 测试用虚拟 skill | — | — |
| **loop** | 循环执行 prompt/命令 | 用户要 "every 5m check X"、"poll status" | 需 `feature('AGENT_TRIGGERS')` |
| **schedule** | 远程 agent 定时触发 | 用户要 schedule recurring agent | 需 `feature('AGENT_TRIGGERS_REMOTE')` |
| **claude-api** | 构建 Claude API/SDK 应用 | 代码导入 `anthropic`/`@anthropic-ai/sdk`；用户问 Claude API 用法 | 需 `feature('BUILDING_CLAUDE_APPS')` |
| **claude-in-chrome** | Chrome 扩展集成 | 需 `shouldAutoEnableClaudeInChrome()` | — |

**Feature flag 条件编译**：`loop`、`schedule`、`claude-api`、`dream`、`hunter` 等使用 `require()` 动态导入（而非静态 import），确保在 `feature()` 为 false 时被 dead code elimination 移除。

### SkillTool Prompt（完整原文）

```
Execute a skill within the main conversation

When users ask you to perform tasks, check if any of the available
skills match. Skills provide specialized capabilities and domain knowledge.

When users reference a "slash command" or "/<something>" (e.g., "/commit",
"/review-pr"), they are referring to a skill. Use this tool to invoke it.

How to invoke:
- Use this tool with the skill name and optional arguments
- Examples:
  - `skill: "pdf"` - invoke the pdf skill
  - `skill: "commit", args: "-m 'Fix bug'"` - invoke with arguments
  - `skill: "review-pr", args: "123"` - invoke with arguments
  - `skill: "ms-office-suite:pdf"` - invoke using fully qualified name

Important:
- Available skills are listed in system-reminder messages in the conversation
- When a skill matches the user's request, this is a BLOCKING REQUIREMENT:
  invoke the relevant Skill tool BEFORE generating any other response
- NEVER mention a skill without actually calling this tool
- Do not invoke a skill that is already running
- Do not use this tool for built-in CLI commands (like /help, /clear, etc.)
- If you see a <command-name> tag in the current conversation turn,
  the skill has ALREADY been loaded - follow the instructions directly
  instead of calling this tool again
```

**关键设计**：
- "BLOCKING REQUIREMENT" 强制模型先调用工具再生成文本
- `<command-name>` 标签检测避免重复调用
- 区分 skill（模型触发）和 CLI command（用户手动）

### Skill 列表预算

通过 `formatCommandsWithinBudget()` 压缩到**上下文窗口的 1%**（默认 8000 字符）：
- 每条 skill 描述硬上限 `MAX_LISTING_DESC_CHARS = 250` 字符
- Bundled skill 永远保持完整描述（不截断）
- 其他 skill（plugin/MCP/skill-dir）按比例截断
- 极端超预算 → 非 bundled skill 只保留名称（`- skillName`）
- 描述格式：`- name: description - whenToUse`（合并后截断到 250 字符）

### BundledSkill 结构

```typescript
{
  name: string
  description: string
  whenToUse?: string           // 触发条件
  allowedTools?: string[]      // 工具白名单
  context?: 'inline' | 'fork'  // 执行模式
  hooks?: HooksSettings         // 专属 hooks
  files?: Record<string, string> // 参考文件
  getPromptForCommand: (args, context) => Promise<ContentBlockParam[]>
}
```

## 六、消息格式化关键逻辑

### normalizeMessages()

将多内容块消息拆分为每条一个块的数组（UI 展示用），生成派生 UUID。

### normalizeMessagesForAPI()

> [!note] 10+ 步防御性管道
> 每一步都对应一个真实的 bug 修复——不是过度工程，而是 LLM agent 消息管道所需的健壮性。

API 发送前的标准化，包含 **10+ 步** 后处理管道：

```
原始 messages
    ↓ reorderAttachmentsForAPI()     — 附件向上冒泡到 tool_result/assistant 边界
    ↓ filter isVirtual               — 移除仅 UI 展示的虚拟消息
    ↓ forEach:
    │   ├─ system → 转为 user message（local_command 保留给模型参考）
    │   ├─ user → strip tool_reference / 合并连续 user / strip 失败 PDF/Image
    │   ├─ assistant → normalize tool input / merge same-id / strip caller
    │   └─ attachment → normalizeAttachmentForAPI → merge into prev user
    ↓ relocateToolReferenceSiblings() — 移走 tool_reference 旁的文本兄弟（防止模型学到错误 stop pattern）
    ↓ filterOrphanedThinkingOnlyMessages()
    ↓ filterTrailingThinkingFromLastAssistant()
    ↓ filterWhitespaceOnlyAssistantMessages()
    ↓ ensureNonEmptyAssistantContent()
    ↓ smooshSystemReminderSiblings() — 将 <system-reminder> 文本折叠进 tool_result
    ↓ sanitizeErrorToolResultContent() — 错误 tool_result 仅保留 text
    ↓ appendMessageTag (snip)         — 注入 [id:xxx] 标签（供 SnipTool 引用）
    ↓ validateImagesForAPI()          — 验证图片尺寸
    ↓ ensureToolResultPairing()       — 修复 tool_use/tool_result 配对
```

**最复杂的后处理之一**：`smooshSystemReminderSiblings()` 解决了一个微妙的模型行为问题——当 tool_result 旁边有文本兄弟时，API 渲染为 `</function_results>\n\nHuman:<text>`，模型会学到在 bare tail 处也输出 `Human:` 然后 stop。smoosh 将文本折叠进 tool_result.content 消除了这个异常 pattern。

### Assistant Prefill

最后一条 assistant 消息允许为空（用于 prefill）——系统可发送空 assistant 消息作为模型回复的前缀引导。

### 附件注入

通过 `getAttachmentMessages()` 在每次查询前注入，再由 `normalizeAttachmentForAPI()` 转换为 UserMessage：

#### 完整 Attachment 类型列表

| Attachment 类型 | API 转换方式 | 用途 |
|----------------|-------------|------|
| `relevant_memories` | 每条记忆独立包裹 `<system-reminder>` | 自动记忆注入 |
| `nested_memory` | 单条 `<system-reminder>` | CLAUDE.md 嵌套引用的记忆 |
| `skill_listing` | `<system-reminder>` 包裹技能列表 | Skill 列表供模型匹配 |
| `file` | 模拟 `Read` tool_use + tool_result 对 | @文件引用（文本/图片/PDF/notebook） |
| `directory` | 模拟 `Bash(ls)` tool_use + tool_result 对 | @目录引用 |
| `pdf_reference` | 提示模型使用 `Read` + `pages` 参数 | 大 PDF（>10 页）的引导 |
| `selected_lines_in_ide` | IDE 选区内容（截断 2000 字符） | IDE 选区上下文 |
| `opened_file_in_ide` | 仅文件名通知 | IDE 当前打开的文件 |
| `edited_text_file` | diff snippet 通知 | 用户/linter 修改文件通知 |
| `plan_mode` | 完整 5 阶段工作流指令 | Plan 模式指令注入 |
| `auto_mode` | 自主执行指令 | Auto 模式指令注入 |
| `plan_mode_exit` / `auto_mode_exit` | 退出通知 | 模式切换 |
| `hook_success` / `hook_blocking_error` | hook 输出/错误 | Hook 执行结果 |
| `hook_additional_context` | hook 附加上下文 | Hook 注入的额外信息 |
| `queued_command` | 带 `wrapCommandText()` 包装 | 排队中的用户/系统消息 |
| `agent_listing_delta` | 动态 agent 类型列表 | Agent 类型变更通知 |
| `mcp_instructions_delta` | MCP 服务器使用说明 | MCP 指令变更通知 |
| `deferred_tools_delta` | 延迟工具变更列表 | ToolSearch 工具变更 |
| `diagnostics` | 新诊断问题摘要 | 编辑器诊断注入 |
| `task_status` | 任务完成/运行/停止通知 | 后台任务状态 |
| `todo_reminder` / `task_reminder` | 温和提醒使用任务追踪 | 任务追踪提醒 |
| `output_style` | 激活的输出风格提醒 | 输出风格注入 |
| `token_usage` / `budget_usd` / `output_token_usage` | 用量信息 | 上下文/预算提醒 |
| `compaction_reminder` | 自动压缩已启用通知 | 缓解模型"上下文焦虑" |
| `date_change` | 日期变更通知 | 跨天会话 |

#### 附件转换核心逻辑

```typescript
// 所有附件都通过以下管道:
normalizeAttachmentForAPI(attachment)
    → createUserMessage({ content, isMeta: true })  // 对模型可见,对用户隐藏
    → wrapInSystemReminder()                         // <system-reminder> 包裹
```

**关键设计**：
- **文件附件模拟工具调用**：`file` 类型不是简单地注入内容，而是模拟一对 tool_use + tool_result 消息，让模型"以为"自己之前读过这个文件
- **`isMeta: true`**：标记为元信息，在 UI 中不显示给用户，但模型可见
- **附件重排序**（`reorderAttachmentsForAPI`）：附件向上冒泡直到遇到 tool_result 或 assistant 消息边界，确保附件内容不会被插入到错误位置

## 七、processUserInput 的完整路由逻辑

`processUserInputBase()` 中的路由优先级：

```
1. Ultraplan 关键词检测（feature('ULTRAPLAN') 且非 / 开头）
   → 重写为 /ultraplan <prompt>
2. Bash 模式（mode === 'bash'）
   → processBashCommand()
3. Slash 命令（inputString.startsWith('/')）
   → processSlashCommand()
   - Bridge-safe 命令：remote 来源但通过 isBridgeSafeCommand 检查的命令仍可执行
   - 非 bridge-safe 命令：返回 "isn't available over Remote Control"
4. 普通文本
   → processTextPrompt()
```

每条路径都会：
- 提取附件（`getAttachmentMessages()`）——除非 `skipAttachments`
- 处理粘贴图片（`maybeResizeAndDownsampleImageBlock`）
- 执行 `UserPromptSubmit` hooks（可阻塞/停止/注入上下文）
- 添加图片元数据消息（尺寸、来源路径）

### Hook 拦截机制

`UserPromptSubmit` hooks 在 `processUserInput()` 返回前执行，可以：
- **blockingError** → 取消整个查询，只显示错误
- **preventContinuation** → 保留用户输入到上下文但不发送 API 请求
- **additionalContexts** → 注入额外上下文到消息列表
- Hook 输出截断到 `MAX_HOOK_OUTPUT_LENGTH = 10000` 字符

## 八、实践启示

### 消息格式化的防御性深度

`normalizeMessagesForAPI()` 的 10+ 步管道看似过度工程，但每一步都对应一个真实的 bug：
- `filterOrphanedThinkingOnlyMessages` → session resume 时 thinking block 错位导致 API 400
- `smooshSystemReminderSiblings` → 文本兄弟导致模型学到错误 stop pattern（A/B 测试验证 92%→0%）
- `ensureToolResultPairing` → 流式中断导致 tool_use 无对应 tool_result

**启示**：构建 LLM agent 时，消息管道不是"发送前格式化"这么简单——需要一个健壮的多阶段清洗流程来处理各种边缘情况。

### Attachment 的"模拟工具调用"模式

文件附件被转换为模拟的 tool_use + tool_result 对（而非简单的文本注入），这使模型认为它"之前已经读过这个文件"。这种模式：
1. 利用了模型对 tool 结果的理解能力（结构化数据 vs 自由文本）
2. 避免了模型再次调用 Read 去读已经注入的文件
3. 保持了会话历史的一致性（所有文件内容都来自"工具调用"）

### Skill 预算策略：1% 上下文窗口

将 skill 列表限制在上下文窗口的 1% 是一个精心计算的权衡：
- 太少 → 模型无法发现可用的 skill
- 太多 → 浪费 turn-1 的 `cache_creation` tokens（skill 列表在每轮都重新注入）
- Bundled skill 优先保证完整描述，非 bundled 按比例降级——这是一种"优雅退化"的信息密度管理策略

### `isMeta` 与信息可见性分层

> [!tip] Agent 系统的基础架构模式
> 不同参与者（用户、模型、UI）需要看到不同的信息，三层可见性是实现这一目标的核心抽象。

`isMeta: true` 创造了三层可见性：
1. **用户 + 模型**：普通消息
2. **仅模型**：`isMeta: true`（system-reminder、附件、hook 输出）
3. **仅 UI**：`isVirtual: true`（REPL 内部 tool calls、子 agent 消息）

这种分层是 agent 系统的基础架构模式——不同参与者（用户、模型、UI）需要看到不同的信息。
